//********************************************************************
//	created:	19:10:2011   15:38
//	filename: 	rssservice.cpp
//	author:		tiamo
//	purpose:	rss service
//********************************************************************

#include "stdafx.h"
#include "rssservice.h"

namespace rss
{
	namespace sql
	{
		//
		// load item list
		//
		class LoadItemList : public mysql::SQLRequest
		{
		public:
			//
			// constructor
			//
			LoadItemList(mysql::Database& db) : SQLRequest(db, L"LoadItemList")	{}

			//
			// destructor
			//
			virtual ~LoadItemList()											{}

		private:
			//
			// do request imp
			//
			virtual void doRequestImp();

		public:
			//
			// table name
			//
			UniString														mTableName;

			//
			// item list
			//
			RSSItemList														mItemList;
		};

		//
		// insert new item
		//
		class InsertNewItem : public mysql::SQLRequest
		{
		public:
			//
			// constructor
			//
			InsertNewItem(mysql::Database& db) : SQLRequest(db, L"InsertNewItem") {}

			//
			// destructor
			//
			virtual ~InsertNewItem()										{}

		private:
			//
			// do request imp
			//
			virtual void doRequestImp();

		public:
			//
			// table name
			//
			UniString														mTableName;

			//
			// new item list
			//
			RSSItemList														mNewItemList;

			//
			// added item list
			//
			RSSItemList														mAddedItemList;
		};	
	}

	//
	// initialize
	//
	void Service::initialize()
	{
		//
		// call super
		//
		::Service::initialize();

		//
		// load config
		//
		utils::Config* cfg													= Server::getSingleton()->getConfig();
		mTableName															= cfg->getString(L"config/rss/table", L"u2");
		mFailureDelayTime													= cfg->getInt(L"/config/rss/failure_delay", 120);
		mSuccessDelayTime													= cfg->getInt(L"/config/rss/success_delay",  60);
		mContentURL															= cfg->getString(L"/config/rss/url", L"https://u2.dmhy.org/torrentrss.php?rows=50");

		//
		// load rss item list
		//
		LogInfo(L"rss::Service(%p:%ls): running rss from url(%ls)", this, mName.c_str(), mContentURL.c_str());
		utils::SmartPointer<sql::LoadItemList> loadItemList					= new sql::LoadItemList(mDatabase);
		loadItemList->mTableName											= mTableName;
		loadItemList->setCallback(std::bind(&Service::onLoadItemList, this, std::placeholders::_1));
		Server::getSingleton()->getSQLRequestManager()->pushPendingRequest(loadItemList.get());
	}

	//
	// build item list
	//
	RSSItemList Service::buildItemList(utils::DataBuffer&& rssContent)
	{
		UniString convertedString16;
		UniString rssString16												= utils::utf8ToWide(static_cast<char const*>(rssContent.getBuffer()), rssContent.getLength());
		for(size_t i = 0; i < rssString16.length(); i ++)
		{
			wchar_t ch														= rssString16[i];
			if(0x09 == ch || ch == 0x0a || ch == 0xd || (ch >= 0x20 && ch <= 0xd7ff) || (ch >= 0xe000  && ch <= 0xfffd))
				convertedString16.append(1, ch);
			else
				LogWarning(L"rss::Service(%p,%ls): removing invalid char(%08x)", this, mName.c_str(), ch);
		}
		Utf8String convertedString8											= utils::wideToUtf8(convertedString16);
		utils::DataBuffer convertedData(convertedString8.c_str(), static_cast<uint32_t>(convertedString8.length()), false);
		utils::XmlParser rssParser;
		rssParser.parse(std::move(convertedData), false);

		//
		// build item list
		//
		RSSItemList itemList;
		bool checkExist														= false;
		std::vector<xmlNodePtr> itemNodes									= rssParser.getNodes(L"/rss/channel/item");
		for(auto it = itemNodes.begin(); it != itemNodes.end(); ++ it)
		{
			xmlNodePtr itemNode												= *it;
			RSSItem rssItem;

			rssItem.mTitle													= rssParser.getString(L"./title", itemNode, &checkExist);
			rssItem.mLink													= rssParser.getString(L"./link", itemNode, &checkExist);
			UniString::size_type offset										= rssItem.mLink.find_last_of(L"=");
			rssItem.mId														= offset == UniString::npos ? 0 : wcstoul(rssItem.mLink.c_str() + offset + 1, nullptr, 0);
			rssItem.mCategory												= rssParser.getString(L"./category", itemNode, &checkExist);
			UniString categoryDomain										= rssParser.getString(L"./category/@domain", itemNode, &checkExist);
			offset															= categoryDomain.find_last_of(L'=');
			rssItem.mCategoryId												= offset == UniString::npos ? 0 : wcstoul(categoryDomain.c_str() + offset + 1, nullptr, 0);
			rssItem.mDescription											= rssParser.getString(L"./description", itemNode, &checkExist);
			rssItem.mPublishDate											= platform::rfc2822StringToTime(rssParser.getString(L"./pubDate", itemNode, &checkExist));
			rssItem.mGuid													= rssParser.getString(L"./guid", itemNode, &checkExist);
			rssItem.mEnclosure												= rssParser.getString(L"./enclosure/@url", itemNode, &checkExist);
			rssItem.mEnclosureLength										= rssParser.getNumber<uint64_t>(L"./enclosure/@length", itemNode, &checkExist);

			LogDebug(L"rss::Service(%p,%ls): [%ls] %ls", this, mName.c_str(), platform::timeToString(rssItem.mPublishDate).c_str(), rssItem.mTitle.c_str());
			itemList.push_back(std::move(rssItem));
		}

		return itemList;
	}

	//
	// compute added item list
	//
	RSSItemList Service::buildAddedItemList(RSSItemList const& oldList, RSSItemList& newList)
	{
		RSSItemList addedItemList;
		for(auto it = newList.begin(); it != newList.end(); ++ it)
		{
			RSSItem& theItem												= *it;
			if(std::find(oldList.begin(), oldList.end(), theItem) == oldList.end())
				addedItemList.push_back(theItem);
		}

		return addedItemList;
	}

	//
	// start refresh timer
	//
	void Service::startRefreshTimer(uint32_t delayTime)
	{
		LogInfo(L"rss::Service(%p:%ls): setup refresh timer(%u)", this, mName.c_str(), delayTime);
		Server::getSingleton()->getTimerManager()->registerTimer(delayTime, true, std::bind(&Service::onGetContentTimer, this, std::placeholders::_1));
	}

	//
	// load item list
	//
	void Service::onLoadItemList(utils::BaseRequest* theRequest)
	{
		//
		// check result
		//
		sql::LoadItemList* loadItemList										= static_cast<sql::LoadItemList*>(theRequest);
		LogInfo(L"rss::Service(%p:%ls): load item list(%u) finished(%d)", this, mName.c_str(), static_cast<uint32_t>(loadItemList->mItemList.size()), theRequest->getResult());
		if(theRequest->getResult() != -1)
			mItemList														= std::move(loadItemList->mItemList);

		//
		// start timer
		//
		startRefreshTimer(1);
	}

	//
	// get content timer
	//
	bool Service::onGetContentTimer(uint32_t elapsedTick)
	{
		//
		// restart failure timer
		//
		auto restartTimer													= utils::makeResourceHelper([this](){startRefreshTimer(mFailureDelayTime);});
		LogInfo(L"rss::Service(%p,%ls): downloading rss(%ls)", this, mName.c_str(), mContentURL.c_str());
		utils::SmartPointer<utils::HttpRequest> getContent					= new utils::HttpRequest(L"GetRSSContentRequest");
		getContent->beginRequest(L"%ls", mContentURL.c_str());
		getContent->setCallback(std::bind(&Service::onGetContent, this, std::placeholders::_1));
		Server::getSingleton()->getHttpRequestManager()->pushPendingRequest(getContent.get());
		restartTimer.dismiss();
		return true;
	}

	//
	// get content
	//
	void Service::onGetContent(utils::BaseRequest* theRequest)
	{
		//
		// restart failure timer
		//
		utils::HttpRequest* getContent										= static_cast<utils::HttpRequest*>(theRequest);
		auto restartTimerOnFailure											= utils::makeResourceHelper([this](){startRefreshTimer(mFailureDelayTime);});

		LogInfo(L"rss::Service(%p,%ls): download rss(%ls) finished(%d)", this, mName.c_str(), mContentURL.c_str(), theRequest->getResult());
		if(theRequest->getResult() == 200)
		{
			//
			// build item list, compute difference
			//
			RSSItemList itemList											= buildItemList(std::move(getContent->getContent()));
			RSSItemList addedItemList										= buildAddedItemList(mItemList, itemList);
			LogInfo(L"rss::Service(%p,%ls): got (%u) items, new(%u)", this, mName.c_str(), static_cast<uint32_t>(itemList.size()), static_cast<uint32_t>(addedItemList.size()));

			if(!addedItemList.empty())
			{
				//
				// insert added item
				//
				utils::SmartPointer<sql::InsertNewItem> insertNewItem		= new sql::InsertNewItem(mDatabase);
				insertNewItem->mTableName									= mTableName;
				insertNewItem->mNewItemList									= mItemList;
				std::copy(addedItemList.begin(), addedItemList.end(), std::front_insert_iterator<RSSItemList>(insertNewItem->mNewItemList));
				insertNewItem->mAddedItemList								= std::move(addedItemList);
				insertNewItem->setCallback(std::bind(&Service::onInsertNewItem, this, std::placeholders::_1));
				Server::getSingleton()->getSQLRequestManager()->pushPendingRequest(insertNewItem.get());
			}
			else
			{
				//
				// restart timer with success delay tick
				//
				startRefreshTimer(mSuccessDelayTime);
			}

			restartTimerOnFailure.dismiss();
		}
	}

	//
	// insert new item
	//
	void Service::onInsertNewItem(utils::BaseRequest* theRequest)
	{
		//
		// callback
		//
		LogInfo(L"rss::Service(%p:%ls): insert new item finished(%d)", this, mName.c_str(), theRequest->getResult());
		sql::InsertNewItem* insertNewItem									= static_cast<sql::InsertNewItem*>(theRequest);
		if(mNewItemCallback)
			mNewItemCallback(this, insertNewItem->mAddedItemList);

		//
		// schedule the next timer
		//
		startRefreshTimer(mSuccessDelayTime);
		mItemList															= std::move(static_cast<sql::InsertNewItem*>(theRequest)->mNewItemList);
	}

	//
	// do request imp
	//
	void sql::LoadItemList::doRequestImp()
	{
		RSSItem theItem;
		mItemList.clear();
		mysql::Statement statement											= mDatabase.createStatement();
		statement.prepareV(L"select item_id, title, link, category, category_id, unix_timestamp(publish), description, guid, enclosure, enclosure_length from %ls", mTableName.c_str());
		statement.bind(L"item_id", false, theItem.mId);
		statement.bind(L"title", false, theItem.mTitle);
		statement.bind(L"link", false, theItem.mLink);
		statement.bind(L"category", false, theItem.mCategory);
		statement.bind(L"category_id", false, theItem.mCategoryId);
		statement.bind(L"publish", false, theItem.mPublishDate);
		statement.bind(L"description", false, theItem.mDescription, 128 * 1024);
		statement.bind(L"guid", false, theItem.mGuid);
		statement.bind(L"enclosure", false, theItem.mEnclosure);
		statement.bind(L"enclosure_length", false, theItem.mEnclosureLength);
		statement.execute();
		while(statement.fetch())
			mItemList.push_back(std::move(theItem));
	}

	//
	// do request imp
	//
	void sql::InsertNewItem::doRequestImp()
	{
		//
		// start transaction
		//
		mDatabase.startTransaction();
		auto rollback														= utils::makeResourceHelper([this](){mDatabase.rollbackTransaction();});

		//
		// insert new item
		//
		mysql::Statement statement											= mDatabase.createStatement();
		statement.prepareV(L"insert ignore into %ls set item_id=?, title=?, link=?, category=?, category_id=?, publish=from_unixtime(?), description=?, guid=?, enclosure=?, enclosure_length=?", mTableName.c_str());
		for(auto it = mAddedItemList.rbegin(); it != mAddedItemList.rend(); ++ it)
		{
			RSSItem& theItem												= *it;
			statement.reset(false, true);
			statement.bind(L"item_id", true, theItem.mId);
			statement.bind(L"title", true, theItem.mTitle);
			statement.bind(L"link", true, theItem.mLink);
			statement.bind(L"category", true, theItem.mCategory);
			statement.bind(L"category_id", true, theItem.mCategoryId);
			statement.bind(L"publish", true, theItem.mPublishDate);
			statement.bind(L"description", true, theItem.mDescription);
			statement.bind(L"guid", true, theItem.mGuid);
			statement.bind(L"enclosure", true, theItem.mEnclosure);
			statement.bind(L"enclosure_length", true, theItem.mEnclosureLength);
			statement.execute();
		}

		//
		// commit
		//
		mDatabase.commitTransaction();
		rollback.dismiss();
	}
}
